package org.opens.config;


import cn.hutool.json.JSONUtil;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.opens.shiro.Realm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 说明:
 * <p>
 *     1. 在@Configuration这种配置类中, 无法使用@Value来装配自定义属性, 会一直都是null, 可以换一种思路实现:
 *      - 实现EnvironmentAware接口;
 *      - 重写setEnvironment方法;
 *      - 自动装载Environment类, 使用这个类就可以获取到配置文件中的属性;
 * </p>
 */
@Configuration
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroConfig {

    private static final String MY_SESSION_ID_NAME = "shiro_session_id";

    private static final Long SESSION_TIME_OUT = 180000L;

    /**
     * 简介:
     *      配置shiro的Shiro的拦截器的工厂
     * @param securityManager
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(
            @Qualifier(value = "securityManager") SecurityManager securityManager
    ) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl("/login");
        shiroFilter.setSuccessUrl("/page");
        //如果一个角色登录之后去访问他没有权限的url, 就会跳转到这个页面.
        shiroFilter.setUnauthorizedUrl("/403");

        Map<String, String> filterMap = new LinkedHashMap<String, String>();
        // anon表示不需要登陆
        filterMap.put("/login/ajax", "anon");
        // logout需要登陆
        filterMap.put("/logout", "logout");
        filterMap.put("/success", "authc");
        // authc限制必须登录
        filterMap.put("/data/**", "authc");
        filterMap.put("/api/**", "authc");
        filterMap.put("/page", "authc");
        // 这是基于角色的控制, 要求登录用户的角色列表中必须有admin这个用户才可以访问
        filterMap.put("/data/admin/*", "authc,roles[admin]");
        // 这是基于资源的控制, 要求用户必须有data:list这个权限才可以访问这个接口
        filterMap.put("/admin/resources/*", "authc,perms[data:list]");
        //这个是角色拦截器, roles[admin]表示只有admin角色可以访问当前url, roles[admin,test]表示admin,test两个角色都可以访问
        filterMap.put("/admin/**", "authc,roles[admin]");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }


    @Bean(name = "securityManager")
    public SecurityManager getSecurityManager(
            @Qualifier(value = "realm") Realm realm,
            @Autowired SessionManager sessionManager
    ) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        //使shiro的sessionManager生效
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    @Bean(name = "realm")
    public Realm getRealm(
            @Qualifier(value = "hashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher
    ) {
        Realm realm = new Realm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher);
        return realm;
    }

    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher getHashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置使用什么哈希算法
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //指明加密次数, 此处设置为2表示设置2次
        hashedCredentialsMatcher.setHashIterations(2);
        //表示是否存储散列后的密码为16进制，需要和生成密码时的一样，默认是base64；
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 加入注解的使用，不加入这个注解不生效
     *
     * AuthorizationAttributeSourceAdvisor，shiro里实现的Advisor类，
     * 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
     */
    @Bean(name = "authorizationAttributeSourceAdvisor")
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            @Qualifier(value = "securityManager") SecurityManager securityManager
    ) {
        AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
        aASA.setSecurityManager(securityManager);
        return aASA;
    }

    /**
     * 自定义sessionManager
     * @return 使用了shiro默认的DefaultWebSessionManager, 但是: 1. 禁用了在url中携带JSESSIONID; 2. 使用了自定义的cookie;
     */
    @Bean(value = "sessionManager")
    public SessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        //去掉shiro登录时url里的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        //设置过期时间
        sessionManager.setGlobalSessionTimeout(SESSION_TIME_OUT);
        //使自定义cookie生效
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(sessionIdCookie());

        return sessionManager;
    }

    /**
     * 配置保存sessionId的cookie
     * 注意：这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理 也需要自己的cookie
     * @return SimpleCookie
     */
    @Bean("sessionIdCookie")
    public SimpleCookie sessionIdCookie(){
        //设置自定义cookie的名称
        SimpleCookie simpleCookie = new SimpleCookie(MY_SESSION_ID_NAME);
        return simpleCookie;
    }

    /**
     * 简介:
     *      测试手动生产md5加密后的值
     * 输出:
     *      0b4778d00284b057c5d5bb6820b08440
     *      {"algorithmName":"MD5","bytes":"C0d40AKEsFfF1btoILCEQA==","empty":false,"iterations":2,"salt":{"bytes":"ZXdheXRlaw==","empty":false}}
     * 注意:
     *      1. SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)函数的参数说明:
     *          - algorithmName:    表示加密的方式
     *          - source            表示密码
     *          - salt              表示加的盐值
     *          - hashIterations    表示加密的次数, 该值为2且algorithmName为md5的话表示md5(md5()).
     * @param args
     */
    public static void main(String[] args) {
        SimpleHash hash = new SimpleHash("md5","zhangym", ByteSource.Util.bytes("ewaytek"), 2);
        System.out.println(ByteSource.Util.bytes("ewaytek"));
        System.out.println(hash.toString());
        System.out.println(JSONUtil.toJsonStr(hash));
    }

}
